//
//  NMOverviewVideoNode.swift
//  NetEaseMusic
//
//  Created by SAGESSE on 2020/2/25.
//  Copyright © 2020 SAGESSE. All rights reserved.
//

import UIKit
import AsyncDisplayKit


open class NMOverviewVideoNode: ASDisplayNode, ASVideoNodeDelegate {
    
    /// You should never set any value on the backing video node. Use exclusivively the video player node to set properties
    public let videoNode: ASVideoNode = .init()
    
    /// The video playback.
    public let playbackNode: ASButtonNode = .init()
    
    /// The video placeholder image node.
    public let placeholderNode: NMNetworkImageNode = .init()
    
    /// The video header info.
    public var headerNode: ASDisplayNode? {
        willSet {
            headerNode?.removeFromSupernode()
            newValue.map {
                addSubnode($0)
            }
            setNeedsLayout()
        }
    }
    
    /// The video footer info.
    public var footerNode: ASDisplayNode? {
        willSet {
            footerNode?.removeFromSupernode()
            newValue.map {
                insertSubnode($0, belowSubnode: playbackNode)
            }
            setNeedsLayout()
        }
    }
    
    
    // MARK: -
    
    
    open var asset: AVAsset?
    
    /// The URL with which the asset was initialized.
    ///
    /// Setting the URL will override the current asset with a newly created AVURLAsset created from the given URL, and AVAsset *asset will point to that newly created AVURLAsset.  Please don't set both assetURL and asset.
    /// - returns: Current URL the asset was initialized or nil if no URL was given.
    open var assetURL: URL?
    
    
    /// When shouldAutoplay is set to true, a video node will play when it has both loaded and entered the "visible" interfaceState.
    /// If it leaves the visible interfaceState it will pause but will resume once it has returned.
    @NSManaged open var shouldAutoplay: Bool
    @NSManaged open var shouldAutorepeat: Bool
    @NSManaged open var shouldAggressivelyRecoverFromStall: Bool
    
    @NSManaged open var muted: Bool
    
    /// Defaults to 10000.
    @NSManaged open var periodicTimeObserverTimescale: Int32
    /// Defaults to AVLayerVideoGravityResizeAspect.
    @NSManaged open var gravity: String
        
    
    open var playerState: ASVideoNodePlayerState = .unknown
    
    open var placeholderImageURL: URL? {
        set { return placeholderNode.url = newValue }
        get { return placeholderNode.url }
    }
    
    
    // MARK: -

    
    public override init() {
        super.init()
        
        self.videoNode.isHidden = false
        self.videoNode.isUserInteractionEnabled = false
        self.videoNode.isLayerBacked = true
        self.videoNode.delegate = self
        self.videoNode.backgroundColor = .black

        self.playbackNode.isUserInteractionEnabled = false
        self.playbackNode.isLayerBacked = true
        self.playbackNode.setImage(#imageLiteral(resourceName: "cm6_play_icon_white_play"), for: .normal)

        self.placeholderNode.isUserInteractionEnabled = false
        self.placeholderNode.isLayerBacked = true
        self.placeholderNode.contentMode = .scaleAspectFill

        self.addSubnode(self.placeholderNode)
        self.addSubnode(self.playbackNode)
    }
    
    open override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        var overlays = [ASLayoutElement]()
        
        overlays.append(placeholderNode)
        overlays.append(videoNode)
        
        headerNode.map {
            overlays.append($0.insets(bottom: .infinity))
        }
        footerNode.map {
            overlays.append($0.insets(top: .infinity))
        }
        
        overlays.append(ASCenterLayoutSpec(centeringOptions: .XY, sizingOptions: .minimumXY, child: playbackNode))
        
        return ASOverlayLayoutSpec.overlay(overlays)
    }
    
    open override func forwardingTarget(for aSelector: Selector!) -> Any? {
        guard videoNode.responds(to: aSelector) else {
            return nil
        }
        return videoNode
    }
    

    // MARK: -

    
    open func play() {
        shouldBePlaying = true
        
        // Display the video node.
        insertSubnode(videoNode, belowSubnode: placeholderNode)
        playbackNode.alpha = 0
        startLoading()
        
        // Marked as loading.
        playerState = .initialLoading
        
        // Asynchronous processing playback.
        Self.queue.async {
            // Has it been cancelled?
            guard self.shouldBePlaying else {
                return
            }
            
            // When the asset URL no any changes, ignore reset action.
            self.videoNode.asset = self.assetURL.map { AVURLAsset(url: $0) } ?? self.asset
            
            // Resume play of video node.
            ASPerformBlockOnMainThread {
                self.videoNode.isHidden = false
                self.videoNode.play()
            }
        }
    }
    
    open func stop() {
        shouldBePlaying = false

        playbackNode.alpha = 1
        stopLoading()
        
        // Pause the video node until the stop animation is finished clean up the context.
        videoNode.pause()
        stopDisplaying(keepingLastFrame: false) {
            Self.queue.async {
                guard !self.shouldBePlaying else {
                    return
                }
                
                // If thet video node is replayed, the new status shall prevail.
                self.videoNode.asset = nil
                
                // Reset video player.
                ASPerformBlockOnMainThread {
                    self.videoNode.isHidden = true
                    self.videoNode.resetToPlaceholder()
                    self.videoNode.removeFromSupernode()
                }

                // Limit the frequency of change the asset.
                Thread.sleep(forTimeInterval: 0.8)
            }
        }
        
        // Marked as unknown.
        playerState = .unknown
    }
    
    
    // MARK: -
    
    
    open func videoNode(_ videoNode: ASVideoNode, didPlayToTimeInterval timeInterval: TimeInterval) {
        // When the loading animation is display, must to fade out of hiding.
        guard !shouldBeLoading else {
            // Pause video play wait for the animation to complete.
            videoNode.pause()
            stopLoading(true) {
                // When the animation is finished, continue playing.
                guard self.shouldBePlaying else {
                    return
                }
                let play = {
                    if self.shouldBePlaying {
                        self.videoNode.play()
                        self.playerState = .playing
                    }
                }

                // When a placeholder is being displayed, you need to add a fade into hide it.
                guard self.placeholderNode.alpha > 0 else {
                    play()
                    return
                }
                
                // Fade in video node.
                self.startDisplaying(play)
            }
            return
        }
    }
    
    open func videoNode(_ videoNode: ASVideoNode, didStallAtTimeInterval timeInterval: TimeInterval) {
        // Redisplay the load animation.
        startLoading()
        playerState = .loading
    }
    open func videoNode(_ videoNode: ASVideoNode, didFailToLoadValueForKey key: String, asset: AVAsset, error: Error) {
        guard shouldBePlaying else {
            return
        }
        // Must make sure end display without loading the animation.
        stopLoading(true) {
            self.stop()
        }
    }
    open func videoDidPlay(toEnd videoNode: ASVideoNode) {
        guard shouldBePlaying else {
            return
        }
        // Must make sure end display without loading the animation.
        stopLoading(true) {
            self.stopDisplaying()
            self.playerState = .finished
        }
    }
    
    
    // MARK: -
    
    
    private func startLoading() {
        // Make a flash animatino for loading node.
        let flash = CAKeyframeAnimation(keyPath: "opacity").then {
            
            $0.values = [1, 0, 1]
            $0.timingFunctions = [
                CAMediaTimingFunction(name: .easeInEaseOut),
                CAMediaTimingFunction(name: .easeInEaseOut)
            ]
            
            $0.duration = loadTimeInterval * 2
            $0.repeatCount = .infinity
        }
        
        // Add or restore loading animation.
        playbackNode.layer.add(flash, forKey: "video-loading-flash")
        shouldBeLoading = true
        
        // When the application into background mode, current execution of the animation will interrupt, so must restore the animation at appliation return forground mode.
        if loadRecover == nil {
            loadRecover = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
                guard self?.shouldBeLoading ?? false else {
                    return
                }
                self?.startLoading()
            }
        }
    }
    private func stopLoading(_ continuoused: Bool = false, complete: (() -> ())? = nil) {
        // If the current state is not loading, ignore.
        guard shouldBeLoading else {
            complete?()
            return
        }
        
        // When stop loading, must clear the observer.
        loadRecover.map {
            NotificationCenter.default.removeObserver($0)
        }
        loadRecover = nil
        shouldBeLoading = false
        
        // If the action don't need a continuous animation, remove loading animation directly.
        guard continuoused else {
            playbackNode.layer.removeAnimation(forKey: "video-loading-flash")
            complete?()
            return
        }
        
        // Make fade out animation for loading node.
        let fadeOut = CAKeyframeAnimation(keyPath: "opacity")
        
        fadeOut.duration = loadTimeInterval * 2
        fadeOut.beginTime = loadTimeInterval

        // Seamlessly connect the loading animation in layer.
        playbackNode.layer.animation(forKey: "video-loading-flash").map {
            let cur = CACurrentMediaTime()
            let diff = cur - $0.beginTime + loadTimeInterval // Animation in opposite direction.
            fadeOut.beginTime = cur - diff.truncatingRemainder(dividingBy: $0.duration)
        }
        
        // Quickly align to the loading animation (aka: [1, 0, 1]).
        fadeOut.values = [0, 1, 0]
        fadeOut.timingFunctions = [
            CAMediaTimingFunction(name: .easeInEaseOut),
            CAMediaTimingFunction(name: .easeInEaseOut)
        ]
        
        fadeOut.delegate = CAAnimationDelegateToBlock.didStop { _, _ in
            complete?()
        }
        
        // Replace the loading animation.
        playbackNode.layer.removeAnimation(forKey: "video-loading-flash")
        playbackNode.layer.add(fadeOut, forKey: "video-loading-fade-out")
    }
    
    private func startDisplaying(_ complete: (() -> ())? = nil) {
        
        placeholderNode.alpha = 0
        placeholderNode.layer.add(fading(placeholderNode.layer.presentation()?.opacity ?? 1, to: 0, duration: 0.2, complete: complete), forKey:  #function)
        headerNode?.alpha = 0
        headerNode?.layer.add(fading(headerNode?.layer.presentation()?.opacity ?? 1, to: 0, duration: 0.2), forKey: #function)
    }
    
    private func stopDisplaying(keepingLastFrame: Bool = true, complete: (() -> ())? = nil) {
        
        shouldBeLoading = keepingLastFrame
        
        playbackNode.alpha = 1
        playbackNode.layer.add(fading(playbackNode.layer.presentation()?.opacity ?? 0, to: 1, duration: loadTimeInterval, complete: complete), forKey: #function)

        headerNode?.alpha = 1
        headerNode?.layer.add(fading(headerNode?.layer.presentation()?.opacity ?? 0, to: 1, duration: 0.3), forKey: #function)
        
        guard !keepingLastFrame else {
            return
        }
        
        placeholderNode.alpha = 1
        placeholderNode.layer.add(fading(placeholderNode.layer.presentation()?.opacity ?? 0, to: 1, duration: 0.3), forKey: #function)
    }
    
    
    // MARK: -

    
    private func fading(_ from: Any?, to: Any?, duration: TimeInterval? = nil, complete: (() -> ())? = nil) -> CABasicAnimation {
        // Make fade in animation.
        let ani = CABasicAnimation(keyPath: "opacity")

        ani.fromValue = from
        ani.toValue = to
        ani.duration = duration ?? loadTimeInterval
        ani.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

        if let complete = complete {
            ani.delegate = CAAnimationDelegateToBlock.didStop { _, _ in
                complete()
            }
        }

        return ani
    }
    

    private var loadRecover: Any?
    private var loadTimeInterval: TimeInterval = 0.6
    
    private var shouldBeLoading: Bool = false
    private var shouldBePlaying: Bool = false
    
    private static let queue = DispatchQueue(label: "com.163.music.video.async")
}
